package com.dbflow5.runtime;

import com.dbflow5.SqlUtils;
import com.dbflow5.config.FlowManager;
import com.dbflow5.query.SQLOperator;
import com.dbflow5.structure.ChangeAction;
import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.ability.IDataAbilityObserver;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.utils.net.Uri;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Description: Listens for [Model] changes. Register for specific
 * tables with [.addModelChangeListener].
 * Provides ability to register and deregister listeners for when data is inserted, deleted, updated, and saved if the device is
 * above [VERSION_CODES.JELLY_BEAN]. If below it will only provide one callback. This is to be paired
 * with the [ContentResolverNotifier] specified in the [DatabaseConfig].
 */
public class FlowContentObserver implements IDataAbilityObserver {

    private final String contentAuthority;
    EventHandler handler;

    private final CopyOnWriteArraySet<OnModelStateChangedListener> modelChangeListeners = new CopyOnWriteArraySet<>();
    private final CopyOnWriteArraySet<OnTableChangedListener> onTableChangedListeners = new CopyOnWriteArraySet<>();
    private final Map<String, Class<?>> registeredTables = new HashMap<>();
    private final Set<Uri> notificationUris = new HashSet<>();
    private final Set<Uri> tableUris = new HashSet<>();

    protected boolean isInTransaction = false;
    private boolean notifyAllUris = false;

    public FlowContentObserver(String contentAuthority, EventHandler handler) {
        this.contentAuthority = contentAuthority;
        this.handler = handler;
    }

    public boolean isSubscribed() {
        return !registeredTables.isEmpty();
    }

    /**
     * Listens for specific model changes. This is only available in [VERSION_CODES.JELLY_BEAN]
     * or higher due to the api of [ContentObserver].
     */
    public interface OnModelStateChangedListener {

        /**
         * Notifies that the state of a [Model]
         * has changed for the table this is registered for.
         *
         * @param table            The table that this change occurred on. This is ONLY available on [VERSION_CODES.JELLY_BEAN]
         * and up.
         * @param action           The action on the model. for versions prior to [VERSION_CODES.JELLY_BEAN] ,
         * the [Action.CHANGE] will always be called for any action.
         * @param primaryKeyValues The array of primary [SQLOperator] of what changed. Call [SQLOperator.columnName]
         * and [SQLOperator.value] to get each information.
         */
        void onModelStateChanged(Class<?> table, ChangeAction action, SQLOperator[] primaryKeyValues);
    }

    interface ContentChangeListener extends OnModelStateChangedListener, OnTableChangedListener{}

    /**
     * If true, this class will get specific when it needs to, such as using all [Action] qualifiers.
     * If false, it only uses the [Action.CHANGE] action in callbacks.
     *
     * @param notifyAllUris notifyAllUris
     */
    public void setNotifyAllUris(boolean notifyAllUris) {
        this.notifyAllUris = notifyAllUris;
    }

    /**
     * Starts a transaction where when it is finished, this class will receive a notification of all of the changes by
     * calling [.endTransactionAndNotify]. Note it may lead to unexpected behavior if called from different threads.
     */
    public void beginTransaction() {
        if (!isInTransaction) {
            isInTransaction = true;
        }
    }

    /**
     * Ends the transaction where it finishes, and will call [.onChange] for Jelly Bean and up for
     * every URI called (if set), or [.onChange] once for lower than Jelly bean.
     */
    public void endTransactionAndNotify() {
        if (isInTransaction) {
            isInTransaction = false;

            synchronized(notificationUris) {
                for (Uri uri : notificationUris) {
                    onChange();
                }
                notificationUris.clear();
            }
            synchronized(tableUris) {
                for (Uri uri : tableUris) {
                    for (OnTableChangedListener onTableChangedListener : onTableChangedListeners) {
                        String authority = uri.getDecodedAuthority();
                        String fragment = uri.getDecodedFragment();
                        onTableChangedListener.onTableChanged(registeredTables.get(authority), ChangeAction.valueOf(fragment));

                    }
                }
                tableUris.clear();
            }
        }
    }

    /**
     * Add a listener for model changes
     *
     * @param modelChangeListener Generic model change events from an [Action]
     */
    public void addModelChangeListener(OnModelStateChangedListener modelChangeListener) {
        modelChangeListeners.add(modelChangeListener);
    }

    /**
     * Removes a listener for model changes
     *
     * @param modelChangeListener Generic model change events from a [Action]
     */
    public void removeModelChangeListener(OnModelStateChangedListener modelChangeListener) {
        modelChangeListeners.remove(modelChangeListener);
    }

    public void addOnTableChangedListener(OnTableChangedListener onTableChangedListener) {
        onTableChangedListeners.add(onTableChangedListener);
    }

    public void removeTableChangedListener(OnTableChangedListener onTableChangedListener) {
        onTableChangedListeners.remove(onTableChangedListener);
    }

    /**
     * Add a listener for model + table changes
     *
     * @param contentChangeListener Generic model change events from an [Action]
     */
    public void addContentChangeListener(ContentChangeListener contentChangeListener) {
        modelChangeListeners.add(contentChangeListener);
        onTableChangedListeners.add(contentChangeListener);
    }

    /**
     * Removes a listener for model + table changes
     *
     * @param contentChangeListener Generic model change events from a [Action]
     */
    public void removeContentChangeListener(ContentChangeListener contentChangeListener) {
        modelChangeListeners.remove(contentChangeListener);
        onTableChangedListeners.remove(contentChangeListener);
    }

    /**
     * Registers the observer for model change events for specific class.
     * @param context context
     * @param table table
     */
    public void registerForContentChanges(Context context, Class<?> table) {
        registerForContentChanges(DataAbilityHelper.creator(context), table);
    }

    /**
     * Registers the observer for model change events for specific class.
     * @param contentResolver contentResolver
     * @param table table
     */
    public void registerForContentChanges(DataAbilityHelper contentResolver, Class<?> table) {
        contentResolver.registerObserver(SqlUtils.getNotificationUri(contentAuthority, table, null, "", null), this);
        REGISTERED_COUNT.incrementAndGet();
        if (!registeredTables.containsValue(table)) {
            registeredTables.put(FlowManager.getTableName(table), table);
        }
    }

    /**
     * Unregisters this list for model change events
     * @param context context
     */
    public void unregisterForContentChanges(Context context) {
        DataAbilityHelper.creator(context).unregisterObserver(null,this);
        REGISTERED_COUNT.decrementAndGet();
        registeredTables.clear();
    }

    @Override
    public void onChange() {
        for (OnModelStateChangedListener onModelStateChangedListener : modelChangeListeners) {
            onModelStateChangedListener.onModelStateChanged(null, ChangeAction.CHANGE, new SQLOperator[]{});
        }

        for (OnTableChangedListener onTableChangedListener : onTableChangedListeners){
            onTableChangedListener.onTableChanged(null, ChangeAction.CHANGE);
        }
    }

    private static final AtomicInteger REGISTERED_COUNT = new AtomicInteger(0);
    private static boolean forceNotify = false;

    /**
     * If we have registered for content changes.
     * @return true if we have registered for content changes. Otherwise we do not notify
     * in [SqlUtils]
     * for efficiency purposes.
     */
    public static boolean shouldNotify() {
        return forceNotify || REGISTERED_COUNT.get() > 0;
    }

    /**
     * Removes count of observers registered, so we do not send out calls when [Model] changes.
     */
    public static void clearRegisteredObserverCount() {
        REGISTERED_COUNT.set(0);
    }

    /**
     * Set the value according to the forceNotify
     * @param forceNotify if true, this will force itself to notify whenever a model changes even though
     * an observer (appears to be) is not registered.
     */
    public static void setShouldForceNotify(boolean forceNotify) {
        FlowContentObserver.forceNotify = forceNotify;
    }
}
